package auth

import (
	"bytes"
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/md5"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"git.mycaigou.com/gfyx/common/cache"
	"git.myscrm.cn/golang/stark/v4"
	"github.com/google/uuid"
	"strconv"
	"strings"
)

const LoginMaxErrorCnt = 10     // 登录错误密码极限值
const LockUserTime = 1800       // 锁定用户登录时间 30 分钟
const LoginUserExpireTime = 600 // 登录错误判断的有效时间 10 分钟
const AESKey = "fxnr3vxul1yao8artm75nldxwxpdf3jv"
const PrefixKeyPasswordErrCnt = "login:err_cnt:"
const PrefixKeyUserLocked = "login:status:"
const PrefixToken = "login:token:"
const PrefixRefreshToken = "login:refresh_token:"
const LoginWayPassword = 1
const LoginWaySmsCode = 2
const LoginWaySso = 3
const Online = 1
const Offline = 2

type Auth struct {
	redisClient *cache.TRedis
}

type UserInfoItem struct {
	UserId        int64  `json:"user_id"`
	UserGuid      string `json:"user_guid"`
	Account       string `json:"account"`
	Name          string `json:"name"`
	Mobile        string `json:"mobile"`
	Email         string `json:"email"`
	Position      string `json:"position"`
	ResetPassword int    `json:"reset_password"`
	Way           int    `json:"way"`
}

type TenantInfo struct {
	TenantName string   `json:"tenant_name"`
	TenantCode string   `json:"tenant_code"`
	Profits    []string `json:"profits"`
}

type LoginUserInfo struct {
	User   UserInfoItem `json:"user"`
	Tenant TenantInfo   `json:"tenant"`
}

type TokenClaim struct {
	Category   string `json:"c"`
	TenantCode string `json:"t"`
	UserId     int64  `json:"u"`
	Token      string `json:"p"`
}

func NewAuth() *Auth {
	cacheRedis, _ := cache.TenantRedis()
	return &Auth{
		redisClient: cacheRedis,
	}
}

// IsLocked 检查用户是否是锁定
func (a *Auth) IsLocked(ctx context.Context, account string) bool {
	cacheKey := PrefixKeyUserLocked + account
	status, _ := a.redisClient.Get(ctx, cacheKey)
	if status == "locked" {
		return true
	} else {
		// 错误次数已经大于指定次数，则锁定 30 分钟
		cnt := a.GetUserLoginErrorCnt(ctx, account)
		if cnt >= LoginMaxErrorCnt {
			stark.Logger.Infof(ctx, "account is locked, cache key:%s", cacheKey)
			_, _ = a.redisClient.Set(ctx, cacheKey, "locked", true, LockUserTime)
			// 清空错误次数
			a.ResetUserLoginErrorCnt(ctx, account)
			return true
		}
	}
	return false
}

// GetUserLoginErrorCnt 获取用户登录次数
func (a *Auth) GetUserLoginErrorCnt(ctx context.Context, account string) int64 {
	cacheKey := PrefixKeyPasswordErrCnt + account
	errCnt, _ := a.redisClient.Get(ctx, cacheKey)
	errCntNum := 0
	if errCnt != "" {
		errCntNum, _ = strconv.Atoi(errCnt)
	}

	return int64(errCntNum)
}

func (a *Auth) IncrUserLoginErrorCnt(ctx context.Context, account string) int64 {
	cacheKey := PrefixKeyPasswordErrCnt + account
	cnt, _ := a.redisClient.Get(ctx, cacheKey)
	if cnt != "" {
		incr, _ := a.redisClient.Incr(ctx, cacheKey)
		return incr
	} else {
		_, _ = a.redisClient.Set(ctx, cacheKey, "1", true, LoginUserExpireTime)
		return 1
	}
}

func (a *Auth) ResetUserLoginErrorCnt(ctx context.Context, account string) bool {
	cacheKey := PrefixKeyPasswordErrCnt + account
	cacheRedis, _ := cache.TenantRedis()
	_, err := cacheRedis.Del(ctx, cacheKey)
	if err != nil {
		stark.Logger.Errorf(ctx, "Del login key:%s, err:%+v", cacheKey, err)
		return false
	}
	return true
}

// VerifyPassword 校验密码
// 验证账号密码是否正确
// 需要兼容建模登录的密码
// 1、Salt为空：Md5(明文密码) == MyUser.Password(Md5)
// 2、Salt不为空：Md5(明文密码+Salt) ==  MyUser.Password(Md5)
func (a *Auth) VerifyPassword(requestPassword, databasePassword, salt string) bool {
	decodePasswordByte, _ := base64.StdEncoding.DecodeString(requestPassword)
	decodePassword := string(decodePasswordByte)
	if len(salt) > 0 {
		decodePassword += salt
	}
	if MD5(decodePassword) != databasePassword {
		return false
	}
	return true
}

// EncryptPassword 加密密码算法
func (a *Auth) EncryptPassword(password string) (string, string) {
	salt := uuid.New().String()
	decodePasswordByte, _ := base64.StdEncoding.DecodeString(password)
	decodePassword := string(decodePasswordByte) + salt

	return MD5(decodePassword), salt
}

func (a *Auth) IssueNewToken(tenantCode string, user LoginUserInfo) (string, string, error) {
	// 生成 token
	token := uuid.New().String()
	refreshToken := uuid.New().String()
	// 编码 token
	eToken, _ := a.encryptToken("t", tenantCode, user.User.UserId, token)
	eRefreshToken, _ := a.encryptToken("re", tenantCode, user.User.UserId, refreshToken)

	return eToken, eRefreshToken, nil
}

// SetTokenToCache 设置缓存
func (a *Auth) SetTokenToCache(ctx context.Context, token string, user LoginUserInfo, expireTime int) (int, error) {
	jsonUser, err := json.Marshal(user)
	if err != nil {
		// 错误处理
		return 0, err
	}
	// token 默认 2 个小时
	res, err := a.redisClient.Set(ctx, PrefixToken+token, string(jsonUser), true, expireTime)
	if err != nil {
		return 0, err
	}
	return res, nil
}

// SetRefreshTokenToCache 设置 refresh token
func (a *Auth) SetRefreshTokenToCache(ctx context.Context, refreshToken, token string, expireTime int) (int, error) {
	// refresh token 比 token 加一个小时
	res, err := a.redisClient.Set(ctx, PrefixRefreshToken+refreshToken, token, true, expireTime)
	if err != nil {
		return 0, err
	}
	return res, nil
}

func (a *Auth) DelTokenToCache(ctx context.Context, token string) (int, error) {
	return a.redisClient.Del(ctx, PrefixToken+token)
}

func (a *Auth) DelRefreshTokenToCache(ctx context.Context, refreshToken string) (int, error) {
	return a.redisClient.Del(ctx, PrefixRefreshToken+refreshToken)
}

func (a *Auth) ParseToken(encryptToken string) (*TokenClaim, error) {
	return a.decryptToken(encryptToken)
}

func (a *Auth) GetLoginInfo(ctx context.Context, token string) (*LoginUserInfo, error) {
	cacheKey := PrefixToken + token
	userInfoJson, err := a.redisClient.Get(ctx, cacheKey)
	if err != nil || userInfoJson == "" {
		return nil, errors.New("登录态失效，请重新登录")
	}
	var userInfo LoginUserInfo
	err = json.Unmarshal([]byte(userInfoJson), &userInfo)
	if err != nil {
		return nil, err
	}
	return &userInfo, nil

}

func (a *Auth) encryptToken(t string, tenantCode string, userId int64, token string) (string, error) {
	// 前缀 + 租户code + 账号id + 随机盐值
	tokenKey := fmt.Sprintf("%s:%s:%d:%s", t, tenantCode, userId, token)
	eToken, err := a.encryptAES([]byte(AESKey), tokenKey)
	if err != nil {
		return "", err
	}
	return eToken, nil
}

func (a *Auth) decryptToken(eToken string) (*TokenClaim, error) {
	decryptAES, err := a.decryptAES([]byte(AESKey), eToken)
	if err != nil {
		return nil, err
	}
	claim := &TokenClaim{}
	arr := strings.Split(decryptAES, ":")
	for i := 0; i < len(arr); i++ {
		switch i {
		case 0:
			claim.Category = arr[0]
		case 1:
			claim.TenantCode = arr[1]
		case 2:
			uid, _ := strconv.Atoi(arr[2])
			claim.UserId = int64(uid)
		case 3:
			claim.Token = arr[3]
		}
	}

	return claim, nil
}

// aes 加密
func (a *Auth) encryptAES(key []byte, plaintext string) (string, error) {
	// 转成字节数组
	origData := []byte(plaintext)
	k := key
	// 分组秘钥
	// NewCipher该函数限制了输入k的长度必须为16, 24或者32
	block, _ := aes.NewCipher(k)
	// 获取秘钥块的长度
	blockSize := block.BlockSize()
	// 补全码
	origData = PKCS7Padding(origData, blockSize)
	// 加密模式
	blockMode := cipher.NewCBCEncrypter(block, k[:blockSize])
	// 创建数组
	cryted := make([]byte, len(origData))
	// 加密
	blockMode.CryptBlocks(cryted, origData)
	return base64.StdEncoding.EncodeToString(cryted), nil
}

// aes 解密
func (a *Auth) decryptAES(key []byte, ct string) (string, error) {
	// 转成字节数组
	cratedByte, _ := base64.StdEncoding.DecodeString(ct)
	k := key
	// 分组秘钥
	block, _ := aes.NewCipher(k)
	// 获取秘钥块的长度
	blockSize := block.BlockSize()
	// 加密模式
	blockMode := cipher.NewCBCDecrypter(block, k[:blockSize])
	// 创建数组
	orig := make([]byte, len(cratedByte))
	// 解密
	if len(cratedByte)%blockSize != 0 {
		return "", errors.New("无效 token 格式")
	}
	blockMode.CryptBlocks(orig, cratedByte)
	// 去补全码
	orig = PKCS7UnPadding(orig)
	return string(orig), nil
}

// PKCS7Padding 补码
// AES加密数据块分组长度必须为128bit(byte[16])，密钥长度可以是128bit(byte[16])、192bit(byte[24])、256bit(byte[32])中的任意一个。
func PKCS7Padding(ciphertext []byte, blocksize int) []byte {
	padding := blocksize - len(ciphertext)%blocksize
	padText := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(ciphertext, padText...)
}

// PKCS7UnPadding 去码
func PKCS7UnPadding(origData []byte) []byte {
	length := len(origData)
	unPadding := int(origData[length-1])
	return origData[:(length - unPadding)]
}

func MD5(s string) string {
	sum := md5.Sum([]byte(s))
	return hex.EncodeToString(sum[:])
}
